上一篇已經看過 HolmesGPT 自己掌握 Model → Tool → Result → Model 的執行迴圈。
這篇不再重複 Agent Loop,而是往裡面再拆一層:
HolmesGPT 到底怎麼設計 Tool?
HolmesGPT 並不是把所有 Function 放進一個 List,再全部交給模型,而是在中間做了幾層處理:
Toolset
↓
ToolsetManager
↓
ToolExecutor
↓
Tool Schema
↓
LLM
↓
Tool.invoke()
↓
真正執行
↓
StructuredToolResult
HolmesGPT 的 Tool 不只是 Function,而是一套從能力註冊、可用性檢查、執行控制,到 Result 回傳都由 Runtime 管理的 Capability System。
這篇主要看其中五個設計。
最簡單的 Agent Tool 寫法,可能是:
tools = [
get_pods,
query_prometheus,
run_bash,
search_web,
]
全部 Function 放進同一個 List,再交給模型。
HolmesGPT 沒有直接這樣做。
它先定義 Toolset(holmes/core/tools.py)。
概念上可以理解成:
Kubernetes Toolset
├── get pods
├── get logs
├── get events
└── ...
Prometheus Toolset
├── query
└── query range
Bash Toolset
└── execute command
如果只看到這裡,Toolset 很像只是分類。
但真正看 Toolset 的欄位,會發現它除了 tools,還管理:
enabled
status
config
prerequisites
tags
approval_required_tools
...
例如 Prometheus Toolset 並不只有「查 Prometheus」這支 Function,它還牽涉:
Prometheus endpoint
Authentication
Config
Network
Runtime 是否能連線
所以 Repo 裡「有 Prometheus Tool」和「這台 HolmesGPT 現在真的能使用 Prometheus」是兩回事。
這也是 Toolset 這層 abstraction 的價值:
把 Tool 和它需要的執行環境一起管理。
同樣的概念也適用在 Kubernetes、Grafana、Database 或其他外部系統。
有了 Toolset 之後,下一個問題是:
Repo 裡有 Kubernetes、Prometheus、Grafana 這些 Toolset,是不是代表模型全部都看得到?
不是。
HolmesGPT 會先經過 ToolsetManager(holmes/core/toolset_manager.py)。
它負責載入 Built-in / Custom Toolset,再根據:
enabled
config
tag
prerequisite
判斷目前哪些 Toolset 可以使用。
最核心的流程在:
ToolsetManager._list_all_toolsets()
例如 CLI 模式下,程式會嘗試 Auto-enable Toolset,但如果必要 Config 根本沒有提供,就不會啟用:
if enable_all_toolsets:
for toolset in toolsets_by_name.values():
if not toolset.missing_config:
toolset.enabled = True
接著還會檢查 Toolset 的 Prerequisite。
所以這裡第一個重要區別是:
Repo 裡有這個 Tool
≠
目前 Runtime 可以使用這個 Tool
例如 Repo 雖然有 Prometheus Toolset,但如果必要的 Config 或執行條件不存在,就不應該讓後面的 Agent 把它當成正常能力。
這裡才是最重要的一段。
ToolsetManager 整理完 Toolset 後,會交給 ToolExecutor(holmes/core/tools_utils/tool_executor.py)。
ToolExecutor 初始化時,第一件事就是只留下:
self.enabled_toolsets = [
ts for ts in toolsets
if ts.status == ToolsetStatusEnum.ENABLED
]
也就是:
ToolsetManager
↓
所有 Toolset 的 Status
↓
ToolExecutor
↓
只留下 ENABLED Toolset
接著再把這些 Tool 收進:
self.tools_by_name
形成真正可用的 Tool Registry。
概念上:
Kubernetes ENABLED
Prometheus ENABLED
Grafana FAILED
Datadog DISABLED
↓
ToolExecutor
↓
get_pods
get_logs
query_prometheus
Grafana 和 Datadog 不會只是「Prompt 告訴模型不要用」。
它們根本不會進入這批可用 Tool。
ToolExecutor 接著透過:
get_all_tools_openai_format()
把 Registry 裡的 Tool 轉成 Model 可以使用的 Function Schema:
return [
tool.get_openai_format()
for tool in self.tools_by_name.values()
]
最後 ToolCallingLLM 取得 Tool 的地方也很直接(holmes/core/tool_calling_llm.py):
def _get_tools(self):
return self.tool_executor.get_all_tools_openai_format(...)
所以完整 Code Path 其實是:
ToolsetManager
│
│ 判斷 Config / Tag / Prerequisite / Status
▼
Toolset
│
│ 只有 status = ENABLED
▼
ToolExecutor
│
│ 建立 tools_by_name
▼
get_all_tools_openai_format()
│
▼
ToolCallingLLM
│
▼
LLM
這裡就可以很清楚看到 HolmesGPT 的一個設計特色:
LLM 並不是看到所有 Repo 裡存在的 Tool,再靠自己決定哪些不能用;Runtime 會先縮小 Capability Surface,最後只把可用的 Tool Schema 交給 Model。
所以與其說 ToolsetManager 是「Tool Loader」,我覺得更準確的理解是:
它參與決定這個 Agent 目前真正擁有哪些能力。
Model 還是可以在這些能力裡自由決定下一步要用哪個 Tool,但「有哪些能力可以選」,已經先由 Code 限制好了。
Tool.invoke()Tool 準備提供給 LLM 時,HolmesGPT 會透過 get_openai_format() 把 Tool 轉成模型可以理解的 Function Schema(holmes/core/tools.py)。
模型看到的不是 Python Function 本身,而比較像:
{
"type": "function",
"function": {
"name": "get_pods",
"description": "Get Kubernetes pods",
"parameters": {
"type": "object",
"properties": {
"namespace": {
"type": "string"
}
}
}
}
}
也就是:
Tool Name
+
Description
+
Parameter Schema
HolmesGPT 的 ToolParameter 也可以描述:
required
object
array
enum
minimum
maximum
pattern
anyOf
...
所以 Model 和 Runtime 之間不是靠自由文字約定,而是有一份正式的 Input Contract。
不過我覺得這還不是 HolmesGPT Tool 最有意思的地方。
真正值得看的,是模型產生 Tool Call 之後發生什麼。
HolmesGPT 所有 Tool 都繼承 Tool。
真正核心的是:
Tool.invoke()
(holmes/core/tools.py)
簡化後,它的流程大概是:
Tool Call
↓
Tool.invoke()
↓
Approval Check
↓
Parameter Processing
↓
_invoke()
↓
Result Processing
真正實作 Tool 行為的是:
_invoke()
例如:
打 API
執行 Command
查 Kubernetes
查 Prometheus
呼叫外部服務
但 Runtime 不會直接跳進 _invoke()。
外面一定先經過:
invoke()
所以可以很簡單地分:
invoke()
= Runtime Contract
_invoke()
= Tool 真正的 Business Logic
這個設計很值得注意。
因為 Tool 作者只需要處理:
這支 Tool 到底怎麼完成工作?
而 Runtime 共通問題,例如:
這次需要 Approval 嗎?
Arguments 要不要處理?
Result 要不要再加工?
可以統一留在 Tool.invoke()。
Tool.invoke() 一開始就會檢查這次 Tool Call 是否需要 Approval。
如果需要確認,而且使用者還沒有 Approve,它不會執行 _invoke()。
而是直接回:
APPROVAL_REQUIRED
因此:
LLM
↓
Tool Call
↓
Tool.invoke()
↓
Requires Approval?
↓
YES
↓
APPROVAL_REQUIRED
真正的 Side Effect 還沒發生。
這個位置非常合理。
因為 Gate 就放在:
Model Decision
和:
Real Execution
中間。
所以 HolmesGPT 可以允許模型有很大的自主性:
自己選 Tool
自己填 Parameters
自己規劃下一步
但這不代表模型也自動取得:
最終執行權
Toolset 本身有:
approval_required_tools
例如概念上可以設定:
approval_required_tools:
- run_kubectl_command
那同一個 Toolset 裡:
查詢資訊的 Tool
→ 可以直接執行
會修改系統的 Tool
→ Human Approval
這比:
整個 Kubernetes Toolset 全部允許
或:
整個 Kubernetes Toolset 全部禁止
更實用。
因為 production Agent 很常遇到這種需求:
Read
→ 可以自動
Write
→ 需要確認
Capability 不一定要整組開、整組關。
HolmesGPT 的 Tool 還可以 override:
requires_approval(params, context)
也就是:
同一支 Tool,要不要 Approval,不一定是固定的。
Runtime 可以看這一次的參數。
這對 Bash 特別重要。
因為:
Bash Tool
本身並不能直接被分類成:
安全
或:
危險
真正的風險取決於:
這一次到底執行什麼 Command
所以 HolmesGPT 的設計其實可以做到:
Tool
+
Arguments
→
Execution Policy
而不只是:
Tool Name
→
Allow / Deny
這已經比單純的 Tool Allowlist 細很多。
前面看到:
Tool Schema
Approval
Parameter Processing
這些都在控制 Tool Input。
但 HolmesGPT 連 Output 也沒有直接丟回模型。
它定義了 StructuredToolResult(holmes/core/tools.py)。
其中有明確 Status,例如:
SUCCESS
ERROR
NO_DATA
APPROVAL_REQUIRED
FRONTEND_PAUSE
也可以帶:
data
error
return_code
images
url
params
elapsed_seconds
為什麼這件事重要?
因為下面三種狀況其實完全不同:
Tool 成功執行,但是沒有資料
Tool 執行失敗
Tool 根本還沒執行,正在等 Approval
如果 Tool 一律只回:
"No result"
下一輪 Model 還需要猜:
沒資料?
API 壞了?
Permission Error?
還是操作根本沒被執行?
Structured Result 讓 Runtime 可以把:
Execution State
和:
Actual Data
一起帶回 Agent。
這讓 Tool Result 不只是 Observation Content,也帶著 Observation Status。
SRE Agent 還有一個很現實的問題:
Tool Output 很容易超大。
例如:
kubectl logs
大量 Kubernetes JSON
Prometheus Result
數萬行 Log
如果每次都:
Tool
↓
50,000 lines
↓
直接塞回 LLM
Context 很快就會膨脹。
所以 HolmesGPT 在 _invoke() 執行之後,還會經過 _apply_transformers()(holmes/core/tools.py)。
概念上:
Raw Result
↓
Transformer
↓
Processed Result
↓
LLM
例如進行:
Filtering
Summarization
Truncation
其他 Result Processing
這個設計其實很有 Agent 味。
一般寫 API Client,只要處理:
API 有沒有成功?
Agent Runtime 還必須考慮:
這份結果適不適合再次送進模型?
例如一萬行 Log 對人和程式來說都是合法結果,但對下一輪 LLM 未必是一個好的 Observation。
所以 HolmesGPT 的 Tool abstraction 不只包含:
怎麼取得資料
也包含:
資料取得之後,
怎麼變成下一輪 Model 可以使用的 Context。
這也是為什麼我會覺得它比單純的 Function Calling 多了一層 Runtime 設計。
HolmesGPT 的 Tool 可以來自不同地方。
例如:
Built-in Tool
Custom YAML Tool
Python Tool
MCP Tool
但它們最後盡量被收斂成:
Tool / Toolset
↓
Tool Runtime
↓
Structured Result
例如 YAMLTool 本身仍然繼承 Tool(holmes/core/tools.py)。
所以 Custom YAML 不是:
YAML
↓
直接執行 Shell
而是:
YAML
↓
YAMLTool
↓
Toolset
↓
Tool.invoke()
↓
Result
MCP 也是類似概念。
MCP 解決的是:
外部能力怎麼被接進來?
但 HolmesGPT 還是可以在自己的 Runtime 決定:
這個 Tool 要不要暴露
需不需要 Approval
執行結果怎麼回到 Model
也就是:
Built-in
YAML
Python
MCP
↓
Tool / Toolset
↓
同一套 Execution Contract
這讓 Runtime 不會因為 Integration 來源不同,就出現完全不同的 Tool 行為。
前面都比較像架構。
Bash Tool 是一個很好理解的實際案例,因為它最容易出現 Side Effect。
相關程式在:
holmes/plugins/toolsets/bash/bash_toolset.py
如果模型產生:
bash(command="...")
HolmesGPT 並不是收到之後直接執行。
Bash Tool 會先做 Command Validation。
概念上可以分成:
LLM 產生 Command
↓
Command Validation
↓
┌──────┼──────────────┐
│ │ │
Allow Deny Approval Required
│ │ │
執行 拒絕 等使用者確認
如果 Command 需要人工確認,requires_approval() 會回:
ApprovalRequirement
然後回到共用的:
Tool.invoke()
invoke() 發現:
needs_approval = true
就直接停下。
所以真正的 Command 還沒有被執行。
這裡我覺得設計最實用的地方是:
HolmesGPT 並沒有簡單把:
Bash
分類成:
Dangerous Tool
然後每次都要求確認。
因為不同 Command 的風險差很多。
例如:
pwd
和一個會修改 production resource 的 Command,明顯不應該使用同樣的 Policy。
所以 Bash Tool 可以依照 Command 本身做 Validation。
這就回到前面提到的:
Tool
+
Arguments
→
真正的 Risk
而不是只有:
Tool Name
→
Risk
對企業 Agent 來說,這個差異很重要。
因為很多 Tool 都同時包含 Read 與 Write 能力。
例如:
Database Tool
Cloud Tool
Kubernetes Tool
Internal Admin API
如果只能做到:
整支 Tool 開
或:
整支 Tool 關
最後往往不是權限太大,就是 Agent 幾乎什麼都不能做。
HolmesGPT 的設計至少提供了一個方向:
把 Policy 往實際 Action 與 Arguments 靠近。
_invoke() 裡還會再檢查一次Bash Tool 還有一個我覺得非常值得看的細節。
理論上,前面的:
requires_approval()
已經應該把需要 Approval 的 Command 擋住。
但真正進入 _invoke() 時,Command 還會再 Validation 一次。
也就是:
第一層
Tool.invoke()
↓
Approval Check
後面還有:
第二層
Bash _invoke()
↓
Command Validation
如果一個理論上需要 Approval 的 Command,竟然在沒有 Approval 的狀態下跑進 _invoke(),程式不會假設:
既然已經到這裡,應該沒問題。
而是回 Error。
這就是很典型的:
Defense in Depth
系統不是假設前面那層永遠不可能出 Bug,而是在真正 Side Effect 前,再確認一次。
這一點比:
System Prompt:
危險操作前一定要先問使用者。
可靠很多。
因為 Prompt 解決的是:
Model 應該怎麼行為
這裡解決的是:
就算 Model 或前面的流程出錯,
Real Execution 還能不能發生。
所以我現在會把 HolmesGPT 的 Tool 設計理解成:
它不是替 LLM 掛上一堆 Function,而是替 Agent 建立一個 Capability Runtime。
Model 可以負責:
我要用哪個 Tool?
Arguments 要填什麼?
下一步要查什麼?
Runtime 則負責:
這個能力現在存在嗎?
這次能不能使用?
這個 Action 要不要 Approval?
Input 是否符合 Contract?
真正能不能執行?
Result 要怎麼回到下一輪?
這個切分,比「支援多少 Tool」更值得拿來比較不同 Agent Framework。
因為模型能力會一直變強,Tool 數量也可以一直增加。
但只要 Agent 要開始碰:
Production System
Internal API
Database
Cloud Resource
Command Execution
最終都會遇到同一個問題:
Model 想做一件事,和系統真的允許它做這件事,中間到底隔著什麼?
HolmesGPT 的答案,就是這套 Tool Runtime。